﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;

namespace SSG.Tools
{
    /// <summary>
    ///   Extension for lists, collections, enumerations and all that stuff.
    /// </summary>
    public static class CollectionTools
    {
        /// <summary>
        ///   Is the enumerable empty?
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <returns> </returns>
        public static bool NullSafeIsEmpty<T>(this IEnumerable<T> enumerable)
        {
            return (enumerable == null || !enumerable.Any());
        }

        /// <summary>
        ///   Does the enumerable contain 1..n elements ("is not empty")?
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <returns> </returns>
        public static bool NullSafeContainsElements<T>(this IEnumerable<T> enumerable)
        {
            return (enumerable != null && enumerable.Any());
        }

        /// <summary>
        ///   Is the given <paramref name="element" /> member of <paramref name="enumerable" />?
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <param name="element"> </param>
        /// <returns> </returns>
        public static bool NullSafeContains<T>(this IEnumerable<T> enumerable, object element) where T : class
        {
            // ReSharper disable PossibleMultipleEnumeration
            return (enumerable != null && enumerable.Any() && element is T && enumerable.Contains(element as T));
            // ReSharper restore PossibleMultipleEnumeration
        }

        /// <summary>
        ///   <c>null</c> safe version of <c>First()</c>.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <returns> </returns>
        public static T NullSafeFirst<T>(this IEnumerable<T> enumerable)
        {
            return enumerable == null ? default(T) : enumerable.First();
        }

        /// <summary>
        ///   Is the given index within the valid range for <paramref name="enumerable" />?
        /// </summary>
        public static bool NullSafeIsValidIndex<T>(this IEnumerable<T> enumerable, int index)
        {
            // ReSharper disable PossibleMultipleEnumeration
            if (enumerable.NullSafeIsEmpty()) return false;
            return (index >= 0 && index < enumerable.Count());
            // ReSharper restore PossibleMultipleEnumeration
        }

        /// <summary>
        ///   Number of elements.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <returns> </returns>
        public static int NullSafeCount<T>(this IEnumerable<T> enumerable)
        {
            return (enumerable == null) ? 0 : enumerable.Count();
        }

        /// <summary>
        ///   Number of elements.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="collection"> </param>
        /// <returns> </returns>
        public static int NullSafeCount<T>(this ICollection<T> collection)
        {
            return (collection == null) ? 0 : collection.Count;
        }

        /// <summary>
        ///   Number of elements ("legacy version" for some rare cases).
        /// </summary>
        /// <param name="collection"> </param>
        /// <returns> </returns>
        /// <remarks>
        ///   Overloading of <see cref="NullSafeCount{T}(System.Collections.Generic.ICollection{T})" /> is not possible.
        ///   There would be ambitious calls.
        /// </remarks>
        public static int NullSafeCountNonGenerics(this ICollection collection)
        {
            return (collection == null) ? 0 : collection.Count;
        }

        /// <summary>
        ///   <c>null</c> safe conversion to array.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable"> </param>
        /// <returns> </returns>
        public static T[] NullSafeToArray<T>(this IEnumerable<T> enumerable)
        {
            if (enumerable == null) return null;
            return enumerable.ToArray();
        }

        /// <summary>
        ///   Add a range to a list, without creating duplicates.
        ///   Basically the same as Linq's <c>Union</c>.
        /// </summary>
        /// <typeparam name="TPc"> </typeparam>
        /// <typeparam name="TSc"> </typeparam>
        /// <param name="targetList"> </param>
        /// <param name="range"> </param>
        /// <returns> </returns>
        /// <seealso
        ///   cref="Enumerable.Union{TSource}(System.Collections.Generic.IEnumerable{TSource},System.Collections.Generic.IEnumerable{TSource})" />
        public static int AddRangeWithoutDuplicates<TPc, TSc>(this List<TPc> targetList, IEnumerable<TSc> range) where TSc : TPc
        {
            if (targetList == null) throw new ArgumentNullException("targetList");
            Contract.EndContractBlock();

            // ReSharper disable PossibleMultipleEnumeration
            if (range == null || range.NullSafeIsEmpty()) return 0;
            int count = 0;
            foreach (TSc obj in range) {
                if (targetList.Contains(obj)) continue;
                targetList.Add(obj);
                count++;
            }
            // ReSharper restore PossibleMultipleEnumeration

            return count;
        }

        /// <summary>
        ///   Add range without the need to convert <paramref name="range" /> as in
        ///   <see cref="List{T}.AddRange" />.
        /// </summary>
        /// <typeparam name="TPc"> </typeparam>
        /// <typeparam name="TSc"> </typeparam>
        /// <param name="targetList"> </param>
        /// <param name="range"> </param>
        /// <returns> </returns>
        public static int AddRangeTwoTypes<TPc, TSc>(this List<TPc> targetList, IEnumerable<TSc> range) where TSc : TPc
        {
            if (targetList == null) throw new ArgumentNullException("targetList");
            Contract.EndContractBlock();

            // ReSharper disable PossibleMultipleEnumeration
            if (range == null || range.NullSafeIsEmpty()) return 0;
            int count = 0;
            foreach (TSc obj in range) {
                targetList.Add(obj);
                count++;
            }
            // ReSharper restore PossibleMultipleEnumeration

            return count;
        }

        /// <summary>
        ///   Merge two list, whereby the source list can be a subtype of the target list.
        ///   Basically the same as Linq's <c>Union</c>.
        /// </summary>
        /// <typeparam name="TTarget"> </typeparam>
        /// <typeparam name="TSource"> </typeparam>
        /// <param name="targetList"> </param>
        /// <param name="range"> </param>
        /// <returns> </returns>
        public static int AddSubtypeRangeWithoutDuplicates<TTarget, TSource>(this List<TTarget> targetList, IEnumerable<TSource> range) where TSource : TTarget
        {
            if (targetList == null) throw new ArgumentNullException("targetList");
            Contract.EndContractBlock();

            // ReSharper disable PossibleMultipleEnumeration
            if (range == null || range.NullSafeIsEmpty()) return 0;
            int count = 0;
            foreach (TSource obj in range) {
                if (targetList.Contains(obj)) continue;
                targetList.Add(obj);
                count++;
            }
            // ReSharper restore PossibleMultipleEnumeration
            return count;
        }

        /// <summary>
        ///   Add to a list without storing duplicates.
        ///   Basically the same as Linq's <c>Union</c>.
        /// </summary>
        /// <param name="targetList"> </param>
        /// <param name="newObject"> </param>
        /// <returns> </returns>
        public static bool AddSubtypeWithoutDuplicate<TTarget, TSource>(this List<TSource> targetList, TSource newObject) where TSource : TTarget
        {
            if (targetList == null) throw new ArgumentNullException("targetList");
            if (Equals(newObject, default(TSource))) return false; // null check
            if (!targetList.Contains(newObject)) {
                targetList.Add(newObject);
                return true;
            }
            return false;
        }

        /// <summary>
        ///   Add to a list without storing duplicates.
        ///   Basically the same as Linq's <c>Union</c>.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="targetList"> </param>
        /// <param name="newObject"> </param>
        /// <returns> </returns>
        public static bool AddWithoutDuplicate<T>(this List<T> targetList, T newObject)
        {
            if (targetList == null) throw new ArgumentNullException("targetList");
            if (Equals(newObject, default(T))) return false;
            if (!targetList.Contains(newObject)) {
                targetList.Add(newObject);
                return true;
            }
            return false;
        }

        /// <summary>
        ///   Split a list into 1..n sublists with <paramref name="maxElements" />.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="inputList"> </param>
        /// <param name="maxElements"> </param>
        /// <returns> </returns>
        /// <seealso cref="StringTools.EnumerationToString{T}" />
        public static List<List<T>> SplitList<T>(this List<T> inputList, int maxElements)
        {
            if (inputList == null) return null;
            if (maxElements < 1) throw new ArgumentException("maxElements");
            List<List<T>> splitLists = new List<List<T>>();
            if (maxElements >= inputList.Count) {
                splitLists.Add(inputList);
                return splitLists;
            }
            int blocks = inputList.Count/maxElements;
            if (inputList.Count%maxElements > 0) blocks++;
            for (int b = 0; b < blocks; b++) {
                int si = b*maxElements;
                List<T> subList = (b + 1 == blocks) ? inputList.GetRange(si, inputList.Count - si) : inputList.GetRange(si, maxElements);
                splitLists.Add(subList);
            }
            return splitLists;
        }

        /// <summary>
        ///   Split an array into 1..n arrays with <paramref name="maxElements" />.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="inputArray"> </param>
        /// <param name="maxElements"> </param>
        /// <returns> </returns>
        public static List<T[]> SplitArray<T>(this T[] inputArray, int maxElements)
        {
            if (inputArray == null) return null;
            if (maxElements < 1) throw new ArgumentException("maxElements");
            List<List<T>> splittedLists = inputArray.ToList().SplitList(maxElements);
            if (splittedLists == null) return null;

            List<T[]> returnList = new List<T[]>(splittedLists.Count);
            // ReSharper disable LoopCanBeConvertedToQuery
            foreach (List<T> singleList in splittedLists) {
                returnList.Add(singleList.ToArray());
            }
            // ReSharper restore LoopCanBeConvertedToQuery
            return returnList;
        }

        /// <summary>
        ///   Get the distinct <c>String</c> values of the given property
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="list"> </param>
        /// <param name="propertyName"> </param>
        /// <returns> </returns>
        public static List<string> GetDistinctStringPropertyValueList<T>(this List<T> list, string propertyName)
        {
            if (list == null) return null;
            List<string> values = new List<string>(list.Count);
            PropertyInfo pi = typeof (T).GetProperty(propertyName);
            if (pi == null) throw new ArgumentException("Property " + propertyName + " not found!");
            foreach (T obj in list) {
                string v = (string) pi.GetValue(obj, null);
                if (!String.IsNullOrEmpty(v) && !values.Contains(v)) values.Add(v);
            }
            return values;
        }

        /// <summary>
        ///   Get the distinct <c>String</c> values of the given property.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="list"> </param>
        /// <param name="propertyName"> </param>
        /// <returns> </returns>
        public static string[] GetDistinctStringPropertyValues<T>(this List<T> list, string propertyName)
        {
            List<string> values = list.GetDistinctStringPropertyValueList(propertyName);
            return (values == null) ? null : values.ToArray<string>();
        }

        /// <summary>
        ///   Remove <paramref name="itemsToBeRemoved" /> from the <paramref name="array" />.
        /// </summary>
        /// <typeparam name="TPc"> </typeparam>
        /// <typeparam name="TSc"> </typeparam>
        /// <param name="array"> </param>
        /// <param name="itemsToBeRemoved"> </param>
        /// <returns> </returns>
        public static TPc[] RemoveItems<TPc, TSc>(this TPc[] array, List<TSc> itemsToBeRemoved) where TSc : TPc
        {
            if (array == null) return null;
            List<TPc> arrayList = new List<TPc>(array);
            int diff = arrayList.RemoveItems(itemsToBeRemoved);
            return diff < 1 ? array : arrayList.ToArray();
        }

        /// <summary>
        ///   Remove <paramref name="itemsToBeRemoved" /> from the first <paramref name="list" />.
        /// </summary>
        /// <typeparam name="TPc"> Parent class </typeparam>
        /// <typeparam name="TSc"> Sub class </typeparam>
        /// <param name="list"> </param>
        /// <param name="itemsToBeRemoved"> </param>
        /// <returns> Number of removed elements </returns>
        public static int RemoveItems<TPc, TSc>(this List<TPc> list, List<TSc> itemsToBeRemoved) where TSc : TPc
        {
            if (list.NullSafeIsEmpty() || itemsToBeRemoved.NullSafeIsEmpty()) return 0;
            int oldCount = list.Count;
            foreach (TSc removeMe in itemsToBeRemoved) {
                list.Remove(removeMe);
            }
            return oldCount - list.Count;
        }

        /// <summary>
        ///   Remove the last items (if there is any).
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="list"> </param>
        /// <returns> </returns>
        public static bool RemoveLast<T>(this List<T> list)
        {
            if (list.NullSafeIsEmpty()) return false;
            list.RemoveAt(list.Count - 1);
            return true;
        }

        /// <summary>
        ///   Resize an array.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="arrayIn"> </param>
        /// <param name="newSize"> </param>
        /// <returns> </returns>
        public static T[] ResizeArray<T>(this T[] arrayIn, int newSize)
        {
            T[] outArray = new T[newSize];
            if (arrayIn == null || arrayIn.Length < 1) return outArray;
            for (int i = 0; i < arrayIn.Length && i < outArray.Length; i++) {
                outArray[i] = arrayIn[i];
            }
            return outArray;
        }

        /// <summary>
        ///   Convert to a list
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerator"> </param>
        /// <returns> </returns>
        public static List<T> EnumeratorToList<T>(this IEnumerator<T> enumerator)
        {
            if (enumerator == null) return null;
            List<T> list = new List<T>();
            while (enumerator.MoveNext()) {
                list.Add(enumerator.Current);
            }
            enumerator.Reset();
            return list;
        }

        /// <summary>
        ///   Returns a list of generalized objects.
        /// </summary>
        /// <typeparam name="TS"> </typeparam>
        /// <typeparam name="TG"> </typeparam>
        /// <param name="specializedEnumeration"> </param>
        /// <returns> </returns>
        public static List<TG> GeneralizeEnumeration<TG, TS>(this IEnumerable<TS> specializedEnumeration) where TS : TG
        {
            if (specializedEnumeration == null) return null;
            return specializedEnumeration.Select(specialist => (TG) specialist).ToList();
        }

        /// <summary>
        ///   Returns a list of specialized objects.
        /// </summary>
        /// <typeparam name="TS"> </typeparam>
        /// <typeparam name="TG"> </typeparam>
        /// <param name="generalizedEnumerable"> </param>
        /// <returns> </returns>
        public static List<TS> SpecializeEnumeration<TS, TG>(this IEnumerable<TG> generalizedEnumerable) where TS : TG
        {
            if (generalizedEnumerable == null) return null;
            List<TS> specializedList = new List<TS>();
            // ReSharper disable LoopCanBeConvertedToQuery
            foreach (TG generalist in generalizedEnumerable) {
                if (generalist is TS) specializedList.Add((TS) generalist);
            }
            // ReSharper restore LoopCanBeConvertedToQuery
            return specializedList;
        }

        /// <summary>
        ///   Get the different distinct objects of two <c>IEnumerables</c>.
        ///   This means objects of <paramref name="enumerable1" /> not found in <paramref name="enumerable2" />
        ///   and vice versa.
        /// </summary>
        /// <typeparam name="T"> </typeparam>
        /// <param name="enumerable1"> </param>
        /// <param name="enumerable2"> </param>
        /// <returns> </returns>
        public static IEnumerable<T> DifferentObjects<T>(this IEnumerable<T> enumerable1, IEnumerable<T> enumerable2)
        {
            // ReSharper disable PossibleMultipleEnumeration
            if (enumerable1.NullSafeCount() < 1) return enumerable2;
            if (enumerable2.NullSafeCount() < 1) return enumerable1;
            List<T> differences = new List<T>();
            foreach (T e1 in enumerable1) {
                if (!enumerable2.Contains(e1)) differences.AddWithoutDuplicate(e1);
            }
            foreach (T e2 in enumerable2) {
                if (!enumerable1.Contains(e2)) differences.AddWithoutDuplicate(e2);
            }
            return differences;
            // ReSharper restore PossibleMultipleEnumeration
        }

        /// <summary>
        ///   Get first value of a <paramref name="dictionary" />.
        /// </summary>
        /// <typeparam name="TK"> </typeparam>
        /// <typeparam name="TV"> </typeparam>
        /// <param name="dictionary"> </param>
        /// <returns> </returns>
        public static TV GetFirstValue<TK, TV>(this IDictionary<TK, TV> dictionary)
        {
            if (dictionary == null) return default(TV);
            ICollection<TV> vc = dictionary.Values;

            // ReSharper disable ConditionIsAlwaysTrueOrFalse
            return vc == null ? default(TV) : vc.First();
            // ReSharper restore ConditionIsAlwaysTrueOrFalse
        }

        /// <summary>
        ///   "Deep" compare whether the values are the same, depending on <paramref name="useEquals" /> per reference or also
        ///   per <see cref="object.Equals(object)" />.
        /// </summary>
        /// <typeparam name="TK"> </typeparam>
        /// <typeparam name="TV"> </typeparam>
        /// <param name="enumerable1"> </param>
        /// <param name="enumerable2"> </param>
        /// <param name="useEquals"> </param>
        /// <returns> </returns>
        public static bool NullSafeEqualEnumerations<TK, TV>(this IEnumerable<TK> enumerable1, IEnumerable<TV> enumerable2, bool useEquals = false)
        {
            // ReSharper disable PossibleUnintendedReferenceComparison
            if (enumerable1 == enumerable2) return true;
            if (enumerable1 == null || enumerable2 == null) return false;
            // ReSharper restore PossibleUnintendedReferenceComparison

            // ReSharper disable PossibleMultipleEnumeration
            if (enumerable1.Count() != enumerable2.Count()) return false;

            for (int i = 0; i < enumerable1.Count(); i++) {
                object e1 = enumerable1.ElementAt(i);
                object e2 = enumerable2.ElementAt(i);
                if (e1 == e2) continue;
                if (useEquals && e1 != null && e1.Equals(e2)) continue;
                return false;
            }
            // ReSharper restore PossibleMultipleEnumeration
            return true;
        }
    }
}